<!DOCTYPE html>
<!--
Copyright (c) 2013 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/base/in_memory_trace_stream.html">
<link rel="import" href="/tracing/extras/importer/pako.html">
<link rel="import" href="/tracing/importer/importer.html">
<link rel="import" href="/tracing/model/model.html">

<script>
'use strict';

/**
 * @fileoverview GzipImporter inflates gzip compressed data and passes it along
 * to an actual importer.
 */
tr.exportTo('tr.e.importer', function() {
  const GZIP_MEMBER_HEADER_ID_SIZE = 3;

  const GZIP_HEADER_ID1 = 0x1f;
  const GZIP_HEADER_ID2 = 0x8b;
  const GZIP_DEFLATE_COMPRESSION = 8;

  function _stringToUInt8Array(str) {
    const array = new Uint8Array(str.length);
    for (let i = 0; i < str.length; ++i) {
      array[i] = str.charCodeAt(i);
    }
    return array;
  }

  function GzipImporter(model, eventData) {
    // Normalize the data into an Uint8Array.
    this.inflateAsTraceStream = false;
    if (typeof(eventData) === 'string' || eventData instanceof String) {
      eventData = _stringToUInt8Array(eventData);
    } else if (eventData instanceof ArrayBuffer) {
      eventData = new Uint8Array(eventData);
    } else if (eventData instanceof tr.b.InMemoryTraceStream) {
      // This importer does not support processing general TraceStreams, only
      // InMemoryTraceStreams for now.
      eventData = eventData.data;
      this.inflateAsTraceStream_ = true;
    } else {
      throw new Error('Unknown gzip data format');
    }
    this.model_ = model;
    this.gzipData_ = eventData;
  }

  /**
   * @param {eventData} Possibly gzip compressed data as a string or an
   *                    ArrayBuffer.
   * @return {boolean} Whether obj looks like gzip compressed data.
   */
  GzipImporter.canImport = function(eventData) {
    // This importer does not support processing general TraceStreams, only
    // InMemoryTraceStreams for now.
    if (eventData instanceof tr.b.InMemoryTraceStream) {
      eventData = eventData.header;
    }

    let header;
    if (eventData instanceof ArrayBuffer) {
      header = new Uint8Array(eventData.slice(0, GZIP_MEMBER_HEADER_ID_SIZE));
    } else if (typeof(eventData) === 'string' || eventData instanceof String) {
      header = eventData.substring(0, GZIP_MEMBER_HEADER_ID_SIZE);
      header = _stringToUInt8Array(header);
    } else {
      return false;
    }
    return header[0] === GZIP_HEADER_ID1 &&
        header[1] === GZIP_HEADER_ID2 &&
        header[2] === GZIP_DEFLATE_COMPRESSION;
  };

  /**
   * Inflates (decompresses) the data stored in the given gzip bitstream.
   * @return {string} Inflated data.
   */
  GzipImporter.inflateGzipData_ = function(data) {
    let position = 0;

    function getByte() {
      if (position >= data.length) {
        throw new Error('Unexpected end of gzip data');
      }
      return data[position++];
    }

    function getWord() {
      const low = getByte();
      const high = getByte();
      return (high << 8) + low;
    }

    function skipBytes(amount) {
      position += amount;
    }

    function skipZeroTerminatedString() {
      while (getByte() !== 0) {}
    }

    const id1 = getByte();
    const id2 = getByte();
    if (id1 !== GZIP_HEADER_ID1 || id2 !== GZIP_HEADER_ID2) {
      throw new Error('Not gzip data');
    }
    const compressionMethod = getByte();
    if (compressionMethod !== GZIP_DEFLATE_COMPRESSION) {
      throw new Error('Unsupported compression method: ' + compressionMethod);
    }
    const flags = getByte();
    const haveHeaderCrc = flags & (1 << 1);
    const haveExtraFields = flags & (1 << 2);
    const haveFileName = flags & (1 << 3);
    const haveComment = flags & (1 << 4);

    // Skip modification time, extra flags and OS.
    skipBytes(4 + 1 + 1);

    // Skip remaining fields before compressed data.
    if (haveExtraFields) {
      const bytesToSkip = getWord();
      skipBytes(bytesToSkip);
    }
    if (haveFileName) skipZeroTerminatedString();
    if (haveComment) skipZeroTerminatedString();
    if (haveHeaderCrc) getWord();

    // Inflate the data using pako.
    const inflatedData = pako.inflateRaw(data.subarray(position));

    if (this.inflateAsTraceStream_) {
      return GzipImporter.transformToStream(inflatedData);
    }

    let string;
    try {
      string = GzipImporter.transformToString(inflatedData);
    } catch (err) {
      // It may be the case that inflated data does not fit into a V8 string. In
      // that case, try to transform to a trace stream.
      return GzipImporter.transformToStream(inflatedData);
    }

    if (inflatedData.length > 0 && string.length === 0) {
      // Try transforming to a trace stream.
      return GzipImporter.transformToStream(inflatedData);
    }

    return string;
  };

  GzipImporter.transformToStream = function(data) {
    if (data instanceof Uint8Array) {
      return new tr.b.InMemoryTraceStream(data, false);
    }

    throw new Error(`Cannot transform ${type} to TraceStream.`);
  };

  /**
   * Transforms an array-like object to a string.
   *
   * Note that the following two expressions yield identical results:
   *
   *   GzipImporter.transformToString_(data)
   *   JSZip.utils.transformTo('string', data)
   *
   * We use a custom static method because it is faster and, more importantly,
   * avoids OOMing on large traces. See
   * https://github.com/catapult-project/catapult/issues/2051.
   */
  GzipImporter.transformToString = function(data) {
    if (typeof(data) === 'string') return data;  // We already have a string.

    // Fall back to manual conversion if TextDecoder is not available.
    if (typeof TextDecoder === 'undefined') {
      if (data instanceof ArrayBuffer) {
        data = new Uint8Array(data);
      }

      // Based on JSZip.Utils.stringToArrayLike
      const result = [];
      let chunk = 65536;
      let k = 0;
      const len = data.length;

      while (k < len && chunk > 1) {
        try {
          const chunklen = Math.min(k + chunk, len);
          let dataslice;
          if (data instanceof Array) {
            dataslice = data.slice(k, chunklen);
          } else {
            dataslice = data.subarray(k, chunklen);
          }
          result.push(String.fromCharCode.apply(null, dataslice));
          k += chunk;
        } catch (e) {
          chunk = Math.floor(chunk / 2);
        }
      }

      return result.join('');
    }

    if (data instanceof Array) {
      // TextDecoder requires an ArrayBuffer or an ArrayBufferView.
      data = new Uint8Array(data);
    }

    return new TextDecoder('utf-8').decode(data);
  };

  GzipImporter.prototype = {
    __proto__: tr.importer.Importer.prototype,

    get importerName() {
      return 'GzipImporter';
    },

    /**
     * Called by the Model to check whether the importer just encapsulates
     * the actual trace data which needs to be imported by another importer.
     */
    isTraceDataContainer() {
      return true;
    },

    /**
     * Called by the Model to extract subtraces from the event data. The
     * subtraces are passed on to other importers that can recognize them.
     */
    extractSubtraces() {
      const eventData = GzipImporter.inflateGzipData_(this.gzipData_);
      return eventData ? [eventData] : [];
    }
  };

  tr.importer.Importer.register(GzipImporter);

  return {
    GzipImporter,
  };
});
</script>
